﻿using DotNetExtensions.Core.Encryptions;
using DotNetExtensions.Core.Http.Events;
using DotNetExtensions.Core.Helpers;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace DotNetExtensions.Core.Http
{
    public class FileHttpRequest : IHttpClient, IDisposable
    {
        public bool Busying => true;

        public string Token { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }

        public event FileTransferProgressDelegate Progress;
        public event FileTransferProgressDelegate Completed;
        public event FileTransferProgressDelegate Before;

        private readonly Dictionary<long, CancellationTokenSource> Cancellation = new Dictionary<long, CancellationTokenSource>();
        public HttpClientFactory.HttpClientKind Kind { get => HttpClientFactory.HttpClientKind.File; }
        public long RequestId { get; set; }
        public FileHttpRequest()
        {
            RequestId = 0L;
        }

        public HttpResponse Send(string url, HttpRequest content, Method method = Method.GET)
        {
            HttpResponse httpResponse = new HttpResponse();
            try
            {
                Uri uri = new Uri(url);
                HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
                foreach (var item in content.Headers)
                {
                    request.Headers.Add(item.Name, item.Value);
                }

                foreach (var item in content.Cookies)
                {
                    request.CookieContainer.Add(uri, new System.Net.Cookie(item.Name, item.Value));
                }
                var response = request.GetResponse();

                List<byte> buffer = new List<byte>();
                int bufferLen = 1024;
                using (Stream stream = request.GetResponse().GetResponseStream())
                {
                    if (!stream.CanRead) throw new Exception("stream can't read.");
                    byte[] tempBuffer = new byte[bufferLen];
                    int readLen = 0;
                    while ((readLen = stream.Read(tempBuffer, 0, 1024)) > 0)
                    {
                        buffer.AddRange(tempBuffer.Take(readLen));
                    }
                }

                httpResponse.Content = buffer.ToArray();

                httpResponse.Code = 200;
            }
            catch (Exception ex)
            {
                httpResponse.Code = 300;
                httpResponse.Content = Encoding.UTF8.GetBytes(ex.Message);
            }

            return httpResponse;
        }

        public async Task GetAsync(string url, HttpRequest content, Method method = Method.GET)
        {
            RequestId = Snowflake.Default.NextId();
            var e = new FileTransferArgs(RequestId) { Progress = 0, Size = 0, Cancel = false };
            Before?.Invoke(url, e);
            if (e.Cancel)
            {
                await Task.Factory.StartNew(() =>
                {
                    Completed?.Invoke(url, e);
                });
            }
            else
            {
                await Task.Factory.StartNew(() =>
                {
                    try
                    {
                        var cancellationToken = CreateCancellationToken();

                        long position = 0x0;
                        // use temp file for download
                        string filename = Path.Combine(PathHelper.Temp, $"{Snowflake.Default.NextId()}.temp");

                        // long | 8byt 
                        if (File.Exists(filename))
                        {
                            using (Stream stream = File.OpenRead(filename))
                            {
                                position = stream.Length;
                            }
                        }

                        // chunk size 4M
                        // 1G - 100
                        // 10 - 1000

                        int chunk = 4 * 1024 * 1024;
                        Uri uri = new Uri(url);
                        long transferTotalSize = position;
                        HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri);
                        foreach (var item in content.Headers)
                        {
                            request.Headers.Add(item.Name, item.Value);
                        }

                        foreach (var item in content.Cookies)
                        {
                            request.CookieContainer.Add(uri, new System.Net.Cookie(item.Name, item.Value));
                        }
                        var response = request.GetResponse();
                        e.Size = response.ContentLength;

                        Progress?.Invoke(url, e);

                        if (!e.Cancel)
                        {
                            using (Stream stream = request.GetResponse().GetResponseStream())
                            {
                                if (!stream.CanRead) throw new Exception("stream can't read.");

                                using (FileStream fs = new FileStream(filename, FileMode.Create, FileAccess.Write))
                                {
                                    fs.Position = position;
                                    byte[] byts = new byte[chunk];
                                    int transferLength = 0;

                                    do
                                    {
                                        if (cancellationToken.IsCancellationRequested) break;
                                        if (e.Cancel) break;

                                        transferLength = stream.Read(byts, 0, byts.Length);
                                        fs.Write(byts, 0, transferLength);
                                        // fs.Flush();
                                        transferTotalSize += transferLength;
                                        double progress = (transferTotalSize + 0.1) / (e.Size + 0.1);
                                        e.Progress = Math.Round(progress, 2);
                                        e.Content = byts.Take(transferLength).ToArray();

                                        Progress?.Invoke(url, e);
                                    } while (transferLength > 0);
                                }
                            }
                        }

                        if (transferTotalSize == e.Size)
                        {
                            e.Progress = 1;
                            e.FileName = filename;
                            e.Successed = true;
                        }

                        Completed?.Invoke(url, e);
                    }
                    catch (Exception ex)
                    {
                        Completed?.Invoke(url, new FileTransferArgs(RequestId) { Successed = false, ErrorMessage = ex.Message });
                    }
                    finally
                    {
                        RemoveCancellationToken();
                    }
                });
            }
        }

        public HttpResponse Post(string url, HttpRequest request)
        {
            HttpResponse httpResponse = new HttpResponse();
            try
            {
                string fileMd5 = MD5.Hash(request.Content, 0, request.Content.Length);

                FileChunkOption fileChunkOption = new FileChunkOption
                {
                    File = fileMd5,
                    Index = 0,
                    Chunk = fileMd5,
                    Size = 1
                };
                
                Uri uri = new Uri(url);
                var handle = new HttpClientHandler()
                {
                    UseCookies = true
                };
                HttpClient client = new HttpClient(handle);

                MultipartFormDataContent formData = new MultipartFormDataContent("form");

                foreach (var item in request.Headers)
                {
                    formData.Headers.Add(item.Name, item.Value);
                }

                foreach (var item in request.Cookies)
                {
                    handle.CookieContainer.Add(uri, new System.Net.Cookie(item.Name, item.Value));
                }

                formData.Headers.Remove("Content-Type");
                formData.Headers.TryAddWithoutValidation("Content-Type", "multipart/form-data;boundary=form");
                formData.Add(new StringContent(JsonConvert.SerializeObject(fileChunkOption), Encoding.UTF8, "application/json"), "FileChunkOption");

                formData.Add(new StringContent(fileMd5), "File");
                formData.Add(new StringContent("0"), "Index");
                formData.Add(new StringContent(fileMd5), "Chunk");
                formData.Add(new StringContent("1"), "Size");

                var bytContent = new ByteArrayContent(request.Content, 0, request.Content.Length);
                bytContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data");
                bytContent.Headers.ContentDisposition.Name = "\"chunk\"";
                bytContent.Headers.ContentDisposition.FileName = "\"" + "file" + "\"";
                bytContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
                formData.Add(bytContent);

                if (url.StartsWith("https")) ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072;

                var response = client.PostAsync(uri, formData).Result;
                httpResponse.Content = response.Content.ReadAsByteArrayAsync().Result;
                httpResponse.Code = (int)response.StatusCode;
            }
            catch (Exception ex)
            {
                httpResponse.Code = 300;
                httpResponse.Content = Encoding.UTF8.GetBytes(ex.Message);
            }

            return httpResponse;
        }
        public async Task SendAsync(string url, HttpRequest request, Method method = Method.GET)
        {
            RequestId = Snowflake.Default.NextId();
            var e = new FileTransferArgs(RequestId) { Progress = 0, Size = 0, Cancel = false };
            Before?.Invoke(url, e);
            if (e.Cancel)
            {
                await Task.Factory.StartNew(() =>
                {
                    Completed?.Invoke(url, e);
                });
            }
            else if (string.IsNullOrEmpty(e.FileName) && File.Exists(e.FileName))
            {
                await PostFile(url, e, request);
            }
            else if (request.Content != null && request.Content.Length > 0)
            {
                await Task.Factory.StartNew(() =>
                {
                    var response = Post(url, request);
                    e.Successed = response.OK;
                    e.Progress = 1;

                    Completed?.Invoke(url, e);
                });
            }
            else
            {
                e.ErrorMessage = "File request params invalid.";

                await Task.Factory.StartNew(() =>
                {
                    Completed?.Invoke(url, e);
                });
            }
        }

        private Task PostFile(string url, FileTransferArgs fileTransferArgs, HttpRequest request)
        {
            return Task.Factory.StartNew((args) =>
            {
                try
                {
                    var cancellationToken = CreateCancellationToken();

                    FileTransferArgs e = args as FileTransferArgs;

                    long contentTotalSize = 0;
                    int chunkSize = 1 * 1024 * 1024;

                    string filename = Path.GetFileName(url);
                    
                    int transferChunkIndex = 0;
                    long totalChunkCount = 0;
                    using (FileStream fs = new FileStream(e.FileName, FileMode.Open, FileAccess.Read))
                    {
                        if (!fs.CanRead) throw new Exception("file can't read");
                        long size = fs.Length;
                        e.Size = size;

                        // compute chunk size
                        string fileMd5 = MD5.Hash(fs);
                        fs.Position = 0;
                        totalChunkCount = (size - 1) / chunkSize + 1;

                        byte[] byts = new byte[chunkSize];
                        int len = 0;
                        transferChunkIndex = 0;

                        Uri uri = new Uri(url);
                        var handle = new HttpClientHandler()
                        {
                            UseCookies = true
                        };
                        HttpClient client = new HttpClient(handle);
                        foreach (var item in request.Cookies)
                        {
                            handle.CookieContainer.Add(uri, new System.Net.Cookie(item.Name, item.Value));
                        }
                        do
                        {
                            if (cancellationToken.IsCancellationRequested) break;
                            if (e.Cancel) break;

                            len = fs.Read(byts, 0, byts.Length);

                            if (transferChunkIndex > totalChunkCount) break;
                            if (len == 0) break;

                            string chunkMd5 = MD5.Hash(byts, 0, len);

                            FileChunkOption chunkData = new FileChunkOption
                            {
                                File = fileMd5,
                                Index = transferChunkIndex,
                                Chunk = chunkMd5,
                                Size = Convert.ToInt32(totalChunkCount)
                            };

                            MultipartFormDataContent formData = new MultipartFormDataContent("form");

                            foreach (var item in request.Headers)
                            {
                                formData.Headers.Add(item.Name, item.Value);
                            }

                            formData.Headers.Remove("Content-Type");
                            formData.Headers.TryAddWithoutValidation("Content-Type", "multipart/form-data;boundary=form");

                            formData.Add(new StringContent(JsonConvert.SerializeObject(chunkData), Encoding.UTF8, "application/json"), "FileChunkOption");

                            formData.Add(new StringContent(fileMd5), "File");
                            formData.Add(new StringContent($"{transferChunkIndex}"), "Index");
                            formData.Add(new StringContent(chunkMd5), "Chunk");
                            formData.Add(new StringContent($"{totalChunkCount}"), "Size");

                            if (e.ExtraData != null)
                            {
                                formData.Add(new StringContent(JsonConvert.SerializeObject(e.ExtraData), Encoding.UTF8, "application/json"), "ExtraData");
                            }
                            var bytContent = new ByteArrayContent(byts, 0, len);
                            bytContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data");
                            bytContent.Headers.ContentDisposition.Name = "\"chunk\"";
                            bytContent.Headers.ContentDisposition.FileName = "\"" + filename + "\"";
                            bytContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
                            formData.Add(bytContent);

                            if (url.StartsWith("https")) ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072;

                            var response = client.PostAsync(url, formData, cancellationToken).Result;

                            if (response.StatusCode != HttpStatusCode.OK) break;

                            contentTotalSize += len;
                            double progress = (contentTotalSize + 0.1) / (size + 0.1);
                            e.Progress = Math.Round(progress, 2);
                            Progress?.Invoke(url, e);
                            transferChunkIndex += 1;

                        } while (len > 0);
                    }

                    if (transferChunkIndex == totalChunkCount)
                    {
                        e.Progress = 1;
                        e.Successed = true;
                    }
                    Completed?.Invoke(url, e);
                }
                catch (Exception ex)
                {
                    Completed?.Invoke(url, new FileTransferArgs(RequestId) { ErrorMessage = ex.Message });
                }
                finally
                {
                    RemoveCancellationToken();
                }

            }, fileTransferArgs);
        }

        public void Dispose()
        {
            RequestId = 0L;
            Cancellation.Clear();
        }

        public void Cancel()
        {
            if (Cancellation.ContainsKey(RequestId)) Cancellation[RequestId].Cancel();
        }

        private CancellationToken CreateCancellationToken()
        {
            CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
            CancellationToken cancellationToken = cancellationTokenSource.Token;

            if (!Cancellation.ContainsKey(RequestId)) Cancellation.Add(RequestId, cancellationTokenSource);

            return cancellationToken;
        }

        private void RemoveCancellationToken()
        {
            if (!Cancellation.ContainsKey(RequestId)) Cancellation.Remove(RequestId);
        }

        public HttpResponse Send(string url, Method method = Method.GET)
        {
            return Send(url, new HttpRequest(), method);
        }

        public Task SendAsync(string url, Method method = Method.GET)
        {
            return SendAsync(url, new HttpRequest(), method);
        }
    }
}
